﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using theeg.GeometryWpf;
using theeg.GraphWpf.Models;
using Node = theeg.GraphWpf.Models.Node;

namespace theeg.GraphWpf.Arrangement
{
    public class Force : IArrangement
    {
        private const int MaxRepititions = 100;
        private const double AffectingRange = 10;
        private List<INode> _nodes;
        private NodeArrangementData[] _nodeArrangement;

        public Force(IGraph graph)
        {
            _nodes = graph.Nodes.ToList();
            _nodeArrangement = new NodeArrangementData[_nodes.Count];
        }

        public void Arrange()
        {
            double width = 0;
            double height = 0;
            foreach (var node in _nodes)
            {
                width += node.Width;
                height += node.Height;
            }
            width = Math.Sqrt(width) * 4;
            height = Math.Sqrt(height) * 4;

            var random = new Random(1);
            for (int i = 0; i < _nodeArrangement.Length; i++)
            {
                _nodeArrangement[i] = new NodeArrangementData { X = random.NextDouble() * width, Y = random.NextDouble() * height };
            }

            double c = 8;
            double area = width * height;
            double initialTemperature = 0.1 * Math.Max(width, height);
            double temperature = initialTemperature;
            double k = c * Math.Sqrt(area / Math.Max(_nodes.Count, 1));
            Func<double, double> attractionForce = x => -x * x / k;
            Func<double, double> repulsionForce = x => k * k / Math.Max(1, x);
            for (int r = 0; r < MaxRepititions; r++)
            {
                var hilbertTree = HilbertTree.BuildTree(((IEnumerable<IHilbertTreeNode>)_nodes).ToList());

                for (int i = 0; i < _nodes.Count; i++)
                {
                    INode node = _nodes[i];
                    var arrangementData = _nodeArrangement[i];
                    Point pi = new Point(arrangementData.X, arrangementData.Y);

                    double displacementX = 0.0;
                    double displacementY = 0.0;
                    //get all nodes in range for abhesion
                    var hitResult = new HashSet<IHilbertTreeNode>();
                    double affectedRadius = Math.Max(node.Width, node.Height) * AffectingRange;
                    var queryShape = new ComputationalCircle(pi.X, pi.Y, affectedRadius);
                    HilbertTree.RangeSearch(hilbertTree.GetRoot(), queryShape, hitResult);
                    Point pj;
                    foreach (Node hitNode in hitResult)
                    {
                        var nodeArrangementData = _nodeArrangement[hitNode.GenericMarker];
                        pj = new Point(nodeArrangementData.X, nodeArrangementData.Y);
                        double vx = pi.X - pj.X;
                        double vy = pi.Y - pj.Y;
                        double d = Math.Sqrt(vx * vx + vy * vy);
                        if (d > 0.0)
                        {
                            vx = vx / d;
                            vy = vy / d;
                        }
                        double force = repulsionForce(d);

                        displacementX += vx * force;
                        displacementY += vy * force;
                    }

                    //get all transitions for adhesion
                    var outgoing = (from x in node.OutgoingEdges select x.Target);
                    var incoming = (from x in node.IncomingEdges select x.Source);
                    foreach (var adjacent in incoming.Union(outgoing))
                    {
                        var nodeArrangementData = _nodeArrangement[adjacent.GenericMarker];
                        pj = new Point(nodeArrangementData.X, nodeArrangementData.Y);

                        double vx = pi.X - pj.X;
                        double vy = pi.Y - pj.Y;
                        double d = Math.Sqrt(vx * vx + vy * vy);
                        if (d > 0.0)
                        {
                            vx = vx / d;
                            vy = vy / d;
                        }
                        double force = attractionForce(d);
                        displacementX += vx * force * 0.5;
                        displacementY += vy * force * 0.5;
                    }

                    double distance = Math.Sqrt(displacementX * displacementX + displacementY * displacementY);
                    arrangementData.X += (displacementX / distance) * Math.Min(temperature, distance);
                    arrangementData.Y += (displacementY / distance) * Math.Min(temperature, distance);

                }
                FlushPositions();
                temperature = (1 - r / (double)MaxRepititions) * initialTemperature;
            }
            FlushPositions();
        }

        private void FlushPositions()
        {
            for (int i = 0; i < _nodes.Count; i++)
            {
                _nodes[i].Left = _nodeArrangement[i].X - _nodes[i].Width * 0.5;
                _nodes[i].Top = _nodeArrangement[i].Y - _nodes[i].Height * 0.5;
            }
        }
    }
}